package com.partsoft.webs.spring;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletContext;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.WebApplicationContext;

import com.partsoft.utils.StringUtils;

public class ContextLoader {

	private static final Log logger = LogFactory.getLog(ContextLoader.class);

	private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";

	private static final Properties defaultStrategies;

	static {
		try {
			ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
			defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
		} catch (IOException ex) {
			throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
		}
	}

	private static final Map<ClassLoader, WebApplicationContext> currentContextPerThread = new ConcurrentHashMap<ClassLoader, WebApplicationContext>(
			1);
	private static final Map<ClassLoader, ServletContext> currentServletContextPerThread = new ConcurrentHashMap<ClassLoader, ServletContext>(
			1);

	private String locatorFactorySelector = "classpath*:/META-INF/spring/beanRefContext.xml";

	private String parentContextKey = null;
	
	private String contextClassName = null;

	private String servletConfigLocationParameter = null;

	private String[] configXmlPaths = new String[] { "classpath*:/META-INF/spring/spring-web.xml" };

	private WebApplicationContext context;

	private BeanFactoryReference parentContextRef;
	
	public void setLocatorFactorySelector(String locatorFactorySelector) {
		this.locatorFactorySelector = locatorFactorySelector;
	}
	
	public void setParentContextKey(String parentContextKey) {
		this.parentContextKey = parentContextKey;
	}

	public void setServletConfigLocationParameter(String servletConfigLocationParameter) {
		this.servletConfigLocationParameter = servletConfigLocationParameter;
	}

	public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
		if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
			throw new IllegalStateException(
					"Cannot initialize context because there is already a root application context present - "
							+ "check whether you have multiple ContextLoader* definitions in your web.xml!");
		}
		servletContext.log("Initializing WebApplicationContext");
		long startTime = System.currentTimeMillis();
		try {
			currentServletContextPerThread.put(Thread.currentThread().getContextClassLoader(), servletContext);
			// 装载父级ApplicationContext
			ApplicationContext parent = loadParentContext(servletContext);

			this.context = createWebApplicationContext(servletContext, parent);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
			currentContextPerThread.put(Thread.currentThread().getContextClassLoader(), this.context);

			if (logger.isDebugEnabled()) {
				logger.debug("Published root WebApplicationContext as ServletContext attribute with name ["
						+ WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
			}
			if (logger.isInfoEnabled()) {
				long elapsedTime = System.currentTimeMillis() - startTime;
				logger.info("WebApplicationContext: initialization completed in " + elapsedTime + " ms");
			}

			return this.context;
		} catch (RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
			throw ex;
		} catch (Error err) {
			logger.error("Context initialization failed", err);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
			throw err;
		}

	}

	protected WebApplicationContext createWebApplicationContext(ServletContext servletContext, ApplicationContext parent)
			throws BeansException {

		Class<?> contextClass = determineContextClass(servletContext);
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException("Custom context class [" + contextClass.getName()
					+ "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
		}

		ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils
				.instantiateClass(contextClass);
		wac.setParent(parent);
		wac.setServletContext(servletContext);
		List<String> configPaths = new ArrayList<String>(this.configXmlPaths.length + 1);
		if (StringUtils.hasText(this.servletConfigLocationParameter)) {
			configPaths.add(this.servletConfigLocationParameter);
		}
		configPaths.addAll(Arrays.asList(this.configXmlPaths));
		wac.setConfigLocations(configPaths.toArray(new String[0]));
		customizeContext(servletContext, wac);
		wac.refresh();

		return wac;
	}

	protected Class<?> determineContextClass(ServletContext servletContext) throws ApplicationContextException {
		if (contextClassName != null) {
			try {
				return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
			} catch (ClassNotFoundException ex) {
				throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]",
						ex);
			}
		} else {
			contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
			try {
				return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
			} catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load default context class [" + contextClassName + "]", ex);
			}
		}
	}

	protected void customizeContext(ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) {
	}

	protected ApplicationContext loadParentContext(ServletContext servletContext) throws BeansException {

		ApplicationContext parentContext = null;

		if (parentContextKey != null) {
			BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
			if (logger.isDebugEnabled()) {
				logger.debug("Getting parent context definition: using parent context key of '" + parentContextKey
						+ "' with BeanFactoryLocator");
			}
			this.parentContextRef = locator.useBeanFactory(parentContextKey);
			parentContext = (ApplicationContext) this.parentContextRef.getFactory();
		}

		return parentContext;
	}

	public void closeWebApplicationContext(ServletContext servletContext) {
		servletContext.log("Closing WebApplicationContext");
		try {
			if (this.context instanceof ConfigurableWebApplicationContext) {
				((ConfigurableWebApplicationContext) this.context).close();
			}
		} finally {
			currentContextPerThread.remove(Thread.currentThread().getContextClassLoader());
			currentServletContextPerThread.remove(Thread.currentThread().getContextClassLoader());
			servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
			if (this.parentContextRef != null) {
				this.parentContextRef.release();
			}
		}
	}

	public static WebApplicationContext getCurrentWebApplicationContext() {
		return (WebApplicationContext) currentContextPerThread.get(Thread.currentThread().getContextClassLoader());
	}

	public static ServletContext getCurrentServletContext() {
		return (ServletContext) currentServletContextPerThread.get(Thread.currentThread().getContextClassLoader());
	}

}
